﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IdentityModel.Claims;
using System.IdentityModel.Tokens;
using Sinacor.Infra.Common.Security.Authentication.Credentials;
using System.Data;
using Sinacor.Infra.Service.Data;
using System.Data.Common;
using System.Security.Cryptography;
using PSE.Framework.ProviderLoader;
using PSE.Framework.ProviderLoader.Configuration;

namespace Sinacor.Infra.Service.Security.Authorization.Providers
{
    /// <summary>
    /// Provider de Autorização padrão do Sinacor
    /// </summary>
    public class SinacorAuthorizationProvider : BaseProvider, IAuthorizationProvider
    {
        public SinacorAuthorizationProvider(ProviderConfigurationElement configuration)
            : base(configuration)
        {

        }

        public User RetrieveUser(int userId)
        {
            AccessControl ac = new AccessControl();
            return ac.RetrieveUser(userId);
        }

        public User RetrieveUser(string login)
        {
            AccessControl ac = new AccessControl();
            return ac.RetrieveUser(login);
        }

        /// <summary>
        /// Retorna as Claims de autorização do Token especificado.
        /// </summary>
        /// <param name="token">SecurityToken</param>
        /// <returns>ClaimSet com as claims de autorização</returns>
        public IList<Claim> RetrieveAuthorizationClaims(SecurityToken token)
        {
            List<Claim> claims = null;

            SinacorToken sinacorToken = token as SinacorToken;

            if (sinacorToken == null)
                throw new SecurityTokenValidationException("The token specified is not a SinacorToken");

            claims = GenerateClaims(sinacorToken.UserInfo.CompanyId, AuthorizationClaims.RetrieveUserId(sinacorToken.UserInfo.UserName));

            return claims;
        }

        /// <summary>
        /// Gera as lista dos claims, de acordo com os dados do usuário/empresa
        /// </summary>
        /// <param name="companyId">Id da empresa</param>
        /// <param name="userId">Id do Usuário</param>
        /// <returns>Lista com as claims para o usuário/empresa</returns>
        private List<Claim> GenerateClaims(int companyId, int userId)
        {
            List<Claim> claims = new List<Claim>();

            //Se o token não tem uma empresa, significa que faz parte do processo de autenticação
            //e deve retornar somente os tokens de acesso ao sistema.
            if (companyId == 0)
            {
                claims = GetClaims(userId, companyId, SinacorClaimTypes.Authorization.System);
            }
            //Se o token tiver uma empresa, então retorna as claims referentes ao usuario/empresa.
            else
            {
                //Busca as Claims de Autorização para o usuário.
                claims = GetClaims(userId, companyId);
            }

            return claims;
        }

        /// <summary>
        /// Sobrecarga do método GetClaims, para casos onde não há filtro por ClaimSet.
        /// </summary>
        /// <param name="userId">Id do usuário</param>
        /// <param name="companyId">Id da empresa</param>
        /// <returns>Lista com as claims para o usuário/empresa</returns>
        private List<Claim> GetClaims(int userId, int companyId)
        {
            return GetClaims(userId, companyId, String.Empty);
        }

        /// <summary>
        /// Acessa o banco de dados para retornar as Claims definidas para o usuário
        /// </summary>
        /// <param name="userId">Id do usuário</param>
        /// <param name="companyId">Id do empresa</param>
        /// <param name="claimSet">Tipo de claims que será retornado</param>
        /// <returns>Lista com as claims para o usuário/empresa</returns>
        private List<Claim> GetClaims(int userId, int companyId, string claimSet)
        {
            List<Claim> claims = new List<Claim>();
            OracleDataBase database = null;
            //TODO: MONTAR QUERY COM PERMISSIONAMENTO DO USUÁRIO.
            string query = "SELECT A.DESC_DIREITO,B.DESC_TIPODIREITO FROM TFWDIREITO A " +
                           "INNER JOIN TFWTIPODIREITO B ON A.COD_TIPODIREITO = B.COD_TIPODIREITO ";

            if (!String.IsNullOrEmpty(claimSet))
                query += " WHERE B.DESC_TIPODIREITO = '" + claimSet + "'";

            if (companyId != 0)
                database = new OracleDataBase(companyId);
            else
                database = new OracleDataBase();

            using (DbCommand cmd = database.GetSqlStringCommand(query))
            {
                using (IDataReader reader = database.ExecuteReader(cmd))
                {
                    while (reader.Read())
                        claims.Add(CreateClaim(reader));

                }
            }
            database.Dispose();
            return claims;
        }

        /// <summary>
        /// Cria a claim de acordo com o resultado do banco
        /// </summary>
        /// <param name="reader">IDataReader com o registro da Claim</param>
        /// <returns>Uma nova instância de um Claim com o valor do IDataReader</returns>
        private Claim CreateClaim(IDataReader reader)
        {
            return new Claim(reader["DESC_TIPODIREITO"].ToString(), reader["DESC_DIREITO"].ToString(), Rights.PossessProperty);
        }

        /// <summary>
        /// Método para retornar as funções como string por módulo
        /// </summary>
        /// <param name="userId">Id do usuário</param>
        /// <param name="companyId">Id da empresa</param>
        /// <returns></returns>
        public List<string> RetrieveFunctions(int userId, int companyId, string moduleId)
        {
            List<string> functions = new List<string>();
            try
            {
                string spRetrieveFunctions = "proc_FW.spfw_retornaRoles";

                using (OracleDataBase db = new OracleDataBase(companyId))
                {
                    DbCommand cmd = db.GetStoredProcCommand(spRetrieveFunctions);

                    db.AddInParameter(cmd, "p_cod_usuario", DbType.Int32, userId);
                    db.AddInParameter(cmd, "p_cod_sistema", DbType.String, moduleId);


                    using (IDataReader reader = db.ExecuteReader(cmd, "p_cursor"))
                    {
                        while (reader.Read())
                            functions.Add(reader.GetString(0).Trim());
                    }
                }
            }
            catch (System.Data.OracleClient.OracleException ex)
            {
                if (ex.Code != 6550)
                    throw ex;
            }

            return functions;
        }


        #region RetrieveOperationQuery
        /// <summary>
        /// Retorna a query para verificação da operação
        /// </summary>
        /// <returns>string</returns>
        private string RetrieveOperationQuery(string claimSet)
        {
            return this.RetrieveOperationQuery(claimSet, String.Empty);
        }
        /// <summary>
        /// Retorna a query para verificação da operação
        /// </summary>
        /// <returns>string</returns>
        private string RetrieveOperationQuery(string claimSet, string operation)
        {
            //TODO: MONTAR QUERY COM PERMISSIONAMENTO DO USUÁRIO.
            StringBuilder query = new StringBuilder();
            query.Append("SELECT    A.DESC_DIREITO, ");
            query.Append("          A.COD_DIREITO, ");
            query.Append("          A.COD_TIPO_OPRC, ");
            query.Append("          A.TXT_HASH, ");
            query.Append("          B.COD_TIPODIREITO, ");
            query.Append("          B.DESC_TIPODIREITO, ");
            query.Append("          C.COD_SIST, ");
            query.Append("          C.TXT_HASH TXT_HASH_MODULO ");
            query.Append("FROM      TFWDIREITO A INNER JOIN ");
            query.Append("          TFWTIPODIREITO B ON ");
            query.Append("          A.COD_TIPODIREITO = B.COD_TIPODIREITO LEFT JOIN ");
            query.Append("          TFWDIR_MOD_SIST C ON ");
            query.Append("          A.COD_DIREITO = C.COD_DIR AND ");
            query.Append("          A.COD_TIPODIREITO = C.COD_TIPO_DIR");

            if (!String.IsNullOrEmpty(claimSet) && (!String.IsNullOrEmpty(operation)))
            {
                query.Append(" WHERE B.DESC_TIPODIREITO = '" + claimSet + "' AND ");
                query.Append(" A.DESC_DIREITO = '" + operation + "' ");
            }
            else if ((!String.IsNullOrEmpty(operation)))
                query.Append(" WHERE A.DESC_DIREITO = '" + operation + "' ");
            else if (!String.IsNullOrEmpty(claimSet))
                query.Append(" WHERE B.DESC_TIPODIREITO = '" + claimSet + "'");

            return query.ToString();
        }
        #endregion


        #region retrieveOperations
        /// <summary>
        /// Retorna as operações que o token tem acesso
        /// </summary>
        /// <param name="token"></param>
        /// <returns></returns>
        public List<string> RetrieveOperations(SecurityToken token)
        {
            SinacorToken sinacorToken = (SinacorToken)token;

            //Valida o Token recebido
            if (null == sinacorToken)
                throw new SecurityTokenValidationException("The token specified is not a SinacorToken");

            List<string> operations = new List<string>();
            string operation = string.Empty;

            string query = string.Empty;
            OracleDataBase database = null;

            if (sinacorToken.UserInfo.CompanyId != 0)
            {
                database = new OracleDataBase(sinacorToken.UserInfo.CompanyId);
                query = RetrieveOperationQuery(string.Empty);
            }
            else
            {
                database = new OracleDataBase();
                query = RetrieveOperationQuery(SinacorClaimTypes.Authorization.System);
            }

            using (DbCommand cmd = database.GetSqlStringCommand(query))
            {
                using (IDataReader reader = database.ExecuteReader(cmd))
                {
                    while (reader.Read())
                    {
                        operation = reader.GetValue<string>("DESC_DIREITO");

                        if (!operations.Contains(operation))
                            operations.Add(operation);
                    }
                }
            }
            database.Dispose();
            return operations;
        }
        #endregion

        #region CheckAcessToOperation

        /// <summary>
        /// Verifica se o token tem acesso a uma determinada operação
        /// </summary>
        /// <param name="token"></param>
        /// <param name="operation"></param>
        /// <returns></returns>
        public Operation CheckAcessToOperation(int companyId, string operationName)
        {

            OracleDataBase db = null;
            Operation operation = null;
            SHA384Managed Alg = new SHA384Managed();
            string computedOperationHash = string.Empty;
            string operationHash = string.Empty;
            string module = string.Empty;
            string query = string.Empty;

            if (companyId == 0)
            {
                db = new OracleDataBase();
                query = RetrieveOperationQuery(SinacorClaimTypes.Authorization.System, operationName);
            }
            else
            {
                db = new OracleDataBase(companyId);
                query = RetrieveOperationQuery(String.Empty, operationName);
            }

            using (DbCommand cmd = db.GetSqlStringCommand(query))
            {
                using (IDataReader reader = db.ExecuteReader(cmd))
                {
                    while (reader.Read())
                    {
                        if (operation == null)
                            operation = new Operation();

                        operation.OperationId = reader.GetValue<int>("COD_DIREITO");
                        operation.OperationTypeId = reader.GetValue<int>("COD_TIPODIREITO");
                        operation.Name = reader.GetValue<string>("DESC_DIREITO");
                        operation.Type = reader.GetValue<string>("COD_TIPO_OPRC");
                        operation.Hash = reader.GetValue<string>("TXT_HASH");

                        if (operation.AllowedModules == null)
                            operation.AllowedModules = new List<string>();

                        module = reader.GetValue<string>("COD_SIST");
                        operationHash = reader.GetValue<string>("TXT_HASH_MODULO");

                        computedOperationHash = Convert.ToBase64String(Alg.ComputeHash(Encoding.ASCII.GetBytes(operation.OperationId + "/" + operation.OperationTypeId + "/" + module)));

                        if (operationHash == computedOperationHash)
                            operation.AllowedModules.Add(module);
                    }
                }
            }
            db.Dispose();
            return operation;
        }

        #endregion

        #region IAuthorizationProvider Members

        /// <summary>
        /// Retorna a lista de módulos que um usuário/empresa tem acesso
        /// </summary>
        /// <param name="userId"></param>
        /// <param name="companyId"></param>
        /// <returns></returns>
        public List<string> RetrieveModuleIds(int userId, int companyId)
        {
            if (userId == 0)
                throw new ArgumentException("userId cannot be null", "userId");

            if (companyId == 0)
                throw new ArgumentException("companyId cannot be null", "companyId");

            StringBuilder SQL = new StringBuilder();
            SQL.Append(@"
						SELECT DISTINCT SIS.CD_SISTEMA
						FROM TGESIST SIS 
						INNER JOIN TGESINST INST 
						ON INST.CD_SISTEMA = SIS.CD_SISTEMA 
						INNER JOIN TCASISUSU SISUSU 
						ON SISUSU.CD_SISTEMA = SIS.CD_SISTEMA 
						INNER JOIN TFWMODULOSINST MDINST 
						ON MDINST.CD_SISTEMA = SIS.CD_SISTEMA 
						WHERE 
							SISUSU.IN_BLOQUEIO = 'N' 
							AND SIS.IN_BLOQUEIO='N' 
							AND SISUSU.CD_USUARIO = :CD_USUARIO

						UNION SELECT DISTINCT SIS.CD_SISTEMA
						FROM TGESIST SIS 
						INNER JOIN TGESINST INST 
						ON INST.CD_SISTEMA = SIS.CD_SISTEMA 
						INNER JOIN TCASISGRP SISGRP 
						ON SISGRP.CD_SISTEMA = SIS.CD_SISTEMA 
						INNER JOIN TCAUSUGRP USUGRP 
						ON USUGRP.CD_GRUPO = SISGRP.CD_GRUPO 
						WHERE 
							SISGRP.IN_BLOQUEIO = 'N' 
							AND SIS.IN_BLOQUEIO = 'N' 
							AND USUGRP.CD_USUARIO = :CD_USUARIO");


            List<string> moduleIds = new List<string>();

            using (OracleDataBase dbModule = new OracleDataBase(companyId))
            {
                DbCommand cmd = dbModule.GetSqlStringCommand(SQL.ToString());
                dbModule.AddInParameter(cmd, "CD_USUARIO", DbType.Int32, userId);

                using (IDataReader dr = dbModule.ExecuteReader(cmd))
                {
                    if (dr.Read())
                    {
                        do
                        {
                            moduleIds.Add(dr.GetString(0));
                        }
                        while (dr.Read());
                    }
                }
            }
            return moduleIds;
        }


        /// <summary>
        /// Retorna a lista de módulos instalados
        /// </summary>
        /// <param name="companyId"></param>
        /// <returns></returns>
        public List<string> RetrieveInstalledModuleIds(int companyId)
        {
            StringBuilder SQL = new StringBuilder();
            SQL.Append(@"
						SELECT DISTINCT CD_SISTEMA
						FROM TFWMODULOSINST");

            OracleDataBase dbModule = null;

            if (companyId != 0)
                dbModule = new OracleDataBase(companyId);
            else
                dbModule = new OracleDataBase();

            DbCommand cmd = dbModule.GetSqlStringCommand(SQL.ToString());

            List<string> moduleIds = new List<string>();

            using (IDataReader dr = dbModule.ExecuteReader(cmd))
            {
                if (dr.Read())
                {
                    do
                    {
                        moduleIds.Add(dr.GetString(0));
                    }
                    while (dr.Read());
                }
            }
            dbModule.Dispose();
            return moduleIds;
        }

        #endregion
    }
}
